package org.asiainfo.auth.service.impl;

import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.temp.SaTempUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.asiainfo.auth.domain.vo.LoginVo;
import org.asiainfo.auth.form.PasswordLoginBody;
import org.asiainfo.auth.properties.UserPasswordProperties;
import org.asiainfo.auth.service.IAuthStrategy;
import org.asiainfo.common.core.constant.CacheConstants;
import org.asiainfo.common.core.constant.UserConstants;
import org.asiainfo.common.core.exception.ServiceException;
import org.asiainfo.common.core.exception.user.UserException;
import org.asiainfo.common.core.utils.StringUtils;
import org.asiainfo.common.core.utils.ip.IPUtils;
import org.asiainfo.common.redis.utils.RedisUtils;
import org.asiainfo.common.satoken.utils.LoginHelper;
import org.asiainfo.system.api.RemoteUserService;
import org.asiainfo.system.api.model.LoginUser;
import org.asiainfo.system.api.domain.vo.RemoteClientVo;
import org.springframework.stereotype.Service;

import java.time.Duration;

import static org.asiainfo.common.core.constant.CacheNames.*;

/**
 * 密码策略
 *
 * @author dotor-ww
 */
@Slf4j
@Service("password" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class PasswordAuthStrategy implements IAuthStrategy {

    private final UserPasswordProperties userPasswordProperties;

    @DubboReference
    private RemoteUserService remoteUserService;

    /**
     * 登录
     *
     * @param body 用户信息
     * @return 登录验证信息
     */
    @Override
    public LoginVo login(String body, RemoteClientVo clientVo) {
        PasswordLoginBody loginBody = JSONUtil.toBean(body, PasswordLoginBody.class);
        String username = loginBody.getUsername();
        // 解密密码
        String password = LoginHelper.decryptWithPrivate(loginBody.getPassword());
        // 用户名或密码为空
        if (StringUtils.isBlank(username)) {
            throw new UserException("user.username.not.blank");
        }
        if (StringUtils.isBlank(password)) {
            throw new UserException("user.password.not.blank");
        }
        // 密码不在指定范围内
        if (ObjectUtil.isNotNull(password) && (password.length() < UserConstants.PASSWORD_MIN_LENGTH
            || password.length() > UserConstants.PASSWORD_MAX_LENGTH)){
            throw new UserException("length.not.valid", UserConstants.PASSWORD_MIN_LENGTH, UserConstants.PASSWORD_MAX_LENGTH);
        }
        // 用户名不在指定范围内
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
            throw new UserException("user.password.length.valid", UserConstants.USERNAME_MIN_LENGTH, UserConstants.USERNAME_MAX_LENGTH);
        }
        // IP黑名单校验
        String blackStr = Convert.toStr(RedisUtils.getCacheObject(CacheConstants.SYS_LOGIN_BLACK_IP_LIST));
        if (IPUtils.isMatchedIp(blackStr, IPUtils.getIpAddr())) {
            throw new ServiceException("访问IP已被列入系统黑名单");
        }
        // 查询用户信息
        LoginUser loginUser = remoteUserService.getUserInfo(username);
        // 校验密码
        validatePassword(loginUser, password);
        // 配置参数 Model 登录细节
        SaLoginModel model = new SaLoginModel();
        model.setDevice(clientVo.getDeviceType());
        model.setTimeout(clientVo.getTimeout());
        model.setActiveTimeout(clientVo.getActiveTimeout());
        model.setExtra(CLIENT_KEY, clientVo.getClientId());
        // 登录
        LoginHelper.login(loginUser, model);
        // 处理登录返回结果
        return handleLoginVo(loginUser, clientVo);
    }

    /**
     * 处理登录返回结果
     *
     * @param loginUser 登录用户信息
     * @param clientVo  客户端信息
     * @return 登录返回结果
     */
    private LoginVo handleLoginVo(LoginUser loginUser, RemoteClientVo clientVo) {
        LoginVo loginVo = new LoginVo();
        // accessToken用来鉴权
        loginVo.setAccessToken(StpUtil.getTokenValue());
        // refreshToken用来刷新
        loginVo.setRefreshToken(SaTempUtil.createToken(loginUser.getUserId(), clientVo.getRefreshTimeout()));
        // 设置accessToken过期时间
        loginVo.setExpireIn(StpUtil.getTokenTimeout());
        // 设置refreshToken过期时间
        loginVo.setRefreshExpireIn(clientVo.getRefreshTimeout());
        // 设置客户端信息
        loginVo.setClientId(clientVo.getClientId());
        // 设置租户ID
        loginVo.setTenantId(loginUser.getTenantId());
        return loginVo;
    }

    /**
     * 校验密码
     *
     * @param loginUser 用户信息
     * @param password  密码
     */
    private void validatePassword(LoginUser loginUser, String password) {
        String userName = loginUser.getUsername();
        // 登录重试次数
        Integer retryCount = RedisUtils.getCacheObject(getCacheKey(userName));
        if (ObjectUtil.isNull(retryCount)) {
            retryCount = 0;
        }
        // 重试次数大于最大值
        if (retryCount >= userPasswordProperties.getMaxRetryCount()) {
            throw new UserException("user.password.retry.limit.exceed", userPasswordProperties.getMaxRetryCount(), userPasswordProperties.getLockTime());
        }
        // 判断用户名是否相等
        if (!matches(loginUser, password)) {
            retryCount = retryCount + 1;
            RedisUtils.setCacheObject(getCacheKey(userName), retryCount, Duration.ofMinutes(userPasswordProperties.getLockTime()));
            throw new UserException("user.password.not.match");
        }
        // 清除登录重试次数缓存
        clearLoginRecordCache(userName);
    }

    /**
     * 比较密码
     *
     * @param loginUser 用户信息
     * @param password  密码
     * @return 结果
     */
    private boolean matches(LoginUser loginUser, String password) {
        return LoginHelper.matchesPassword(password, loginUser.getPassword());
    }

    /**
     * 登录账户密码错误次数缓存键名
     *
     * @param username 用户名
     * @return 缓存键key
     */
    private String getCacheKey(String username) {
        return CacheConstants.PWD_ERR_CNT_KEY + username;
    }

    /**
     * 清除登录重试次数
     *
     * @param username 用户名
     */
    private void clearLoginRecordCache(String username) {
        String cacheKey = getCacheKey(username);
        if (RedisUtils.hasKey(cacheKey)) {
            RedisUtils.deleteObject(cacheKey);
        }
    }
}
